/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import * as fs from 'fs';
import * as path from 'path';
import { Json5parser } from '../../utils/common/Json5parser';
import * as semver from 'semver';
import { Module, ModuleCategory } from './moduleComponent';
import { getModuleKind, OH_MODULES_DIR } from '../utils/utils';

export class OHPackageConfig {
  private _projectConfig: OHPackageConfig | undefined;
  private _parameters: Record<string, string> | undefined;
  private _config: ConfigData;
  private _filePath: string;

  public constructor(initialData: ConfigData = {}, filePath: string = '', projectConfig?: OHPackageConfig) {
    this._config = initialData;
    this._filePath = filePath;
    this._projectConfig = projectConfig;
    if (this.getType() === 'project') {
      this.loadParameters();
    }
  }

  public static fromFile(filePath: string, projectConfig?: OHPackageConfig): OHPackageConfig {
    const absolutePath = path.resolve(filePath);
    if (!fs.existsSync(absolutePath)) {
      throw new Error(`File not found: ${absolutePath}`);
    }
    const fileContent = fs.readFileSync(absolutePath, 'utf-8');
    const configData = Json5parser.parseJsonText(fileContent);
    return new OHPackageConfig(configData, absolutePath, projectConfig);
  }

  public save(filePath: string): void {
    this.validate();
    // 使用标准JSON.stringify替代JSON5.stringify，保持基本功能一致
    const fileContent = JSON.stringify(this._config, null, 2);
    fs.writeFileSync(filePath, fileContent, 'utf-8');
  }

  public validate(): void {
    const type = this.getType();
    if (type === 'module') {
      if (!this._config.name) {
        throw new Error('Missing required field: name');
      }
      this.validateName(this._config.name);
      if (!this._config.version) {
        throw new Error('Missing required field: version');
      }
      this.validateVersion(this._config.version);
      if (!this._config.license) {
        throw new Error('Missing required field: license');
      }
      if (!this._config.main) {
        throw new Error('Missing required field: main');
      }
    } else if (type === 'project') {
      if (!this._config.modelVersion) {
        throw new Error('Missing required field: modelVersion');
      }
    } else {
        throw new Error('Unable to determine config type. Missing "name" for module or "modelVersion" for project.');
    }
  }

  private validateName(name: string): void {
    const nameRegex = /^(?:@[a-z][a-z0-9_-]*\/)?[a-z][a-z0-9_.-]*$/;
    if (!nameRegex.test(name)) {
      throw new Error(`Invalid package name: "${name}". It must follow the format [@group]/packagename.`);
    }
    if (name.endsWith('.') || name.endsWith('-') || name.endsWith('_')) {
        throw new Error(`Invalid package name: "${name}". It cannot end with a dot, hyphen, or underscore.`);
    }
  }

  private validateVersion(version: string): void {
    if (!semver.valid(version)) {
      throw new Error(`Invalid version: "${version}". It must follow the semver specification.`);
    }
  }

  public getType(): OHPackageConfigType | 'unknown' {
    if (this._config.name) {
      return 'module';
    }
    if (this._config.modelVersion) {
      return 'project';
    }
    return 'unknown';
  }

  // Common fields
  public getDescription(): string | undefined {
    return this._config.description;
  }

  public setDescription(description: string): void {
    this._config.description = description;
  }

  // Module-specific fields
  public getName(): string {
    if (this.getType() !== 'module') {
      throw new Error('Field "name" is only available for module-level configs.');
    }
    return this._config.name!;
  }

  public setName(name: string): void {
    if (this.getType() !== 'module') {
      throw new Error('Field "name" is only available for module-level configs.');
    }
    this.validateName(name);
    this._config.name = name;
  }

  public getVersion(): string {
    if (this.getType() !== 'module') {
      throw new Error('Field "version" is only available for module-level configs.');
    }
    return this._config.version!;
  }

  public setVersion(version: string): void {
    if (this.getType() !== 'module') {
      throw new Error('Field "version" is only available for module-level configs.');
    }
    this.validateVersion(version);
    this._config.version = version;
  }
  
  public getAuthor(): string | Author | undefined {
    if (this.getType() !== 'module') {
      throw new Error('Field "author" is only available for module-level configs.');
    }
    return this._config.author;
  }

  public setAuthor(author: string | Author): void {
    if (this.getType() !== 'module') {
      throw new Error('Field "author" is only available for module-level configs.');
    }
    this._config.author = author;
  }


  // Dependency management
  public getDependencies(type: 'dependencies' | 'devDependencies' | 'dynamicDependencies'): DependencyObject {
    return this._config[type] || {};
  }

  public addDependency(name: string, version: string, type: 'dependencies' | 'devDependencies' | 'dynamicDependencies'): void {
    if (!this._config[type]) {
      this._config[type] = {};
    }
    this._config[type]![name] = version;
  }

  public removeDependency(name: string, type: 'dependencies' | 'devDependencies' | 'dynamicDependencies'): void {
    if (this._config[type] && this._config[type]![name]) {
      // 创建新对象，使用解构方式排除要删除的属性，保持类型安全
      const { [name]: removed, ...restDependencies } = this._config[type]!;
      this._config[type] = restDependencies as DependencyObject;
    }
  }

  public getRawConfig(): ConfigData {
    return { ...this._config };
  }

  private loadParameters(): void {
    if (this._config.parameterFile) {
      const parameterFilePath = path.resolve(path.dirname(this._filePath), this._config.parameterFile);
      if (fs.existsSync(parameterFilePath)) {
        const fileContent = fs.readFileSync(parameterFilePath, 'utf-8');
        const parsedData = Json5parser.parseJsonText(fileContent);
        // 将unknown类型转换为string类型以匹配Record<string, string>类型
        this._parameters = Object.entries(parsedData).reduce((acc, [key, value]) => {
            acc[key] = String(value);
            return acc;
        }, {} as Record<string, string>);
      } else {
        console.warn(`Parameter file not found: ${parameterFilePath}`);
      }
    }
  }

  private replacePlaceholders(value: string): string {
    if (!value) {
      return '';
    }
    const parameters = this._projectConfig?._parameters;
    if (!parameters) {
      return value;
    }
    
    // 支持两种占位符格式: ${...} 和 @param:...
    const regex = /(\$\{(.*?)\})|(@param:(.*?)(?=[\s,"'\]\}\)]|$))/g;
    
    return value.replace(regex, (match, p1, placeholder1, p2, placeholder2) => {
      // 确定使用哪个占位符内容
      const placeholder = placeholder1 || placeholder2;
      if (!placeholder) {
        return match;
      }
      
      // 支持嵌套对象属性访问，如 dependencies.@ohos/imageknife
      return this.getValueFromNestedObject(parameters, placeholder) || match;
    });
  }
  
  /**
   * 从嵌套对象中获取属性值
   * @param obj 目标对象
   * @param path 属性路径，如 "dependencies.@ohos/imageknife"
   * @returns 属性值或undefined
   */
  /**
   * 递归类型定义，表示嵌套对象结构
   */
  private getValueFromNestedObject(obj: Record<string, unknown>, path: string): string | undefined {
    if (!obj || typeof path !== 'string') {
      return undefined;
    }
    
    // 分割路径
    const parts = path.split('.');
    let current: unknown = obj;
    
    // 遍历路径部分
    for (const part of parts) {
      if (current === undefined || current === null) {
        return undefined;
      }
      
      // 检查当前值是否为对象
      if (typeof current === 'object' && current !== null) {
        current = (current as Record<string, unknown>)[part];
      } else {
        // 如果不是对象，则无法继续访问属性
        return undefined;
      }
    }
    
    // 确保返回的是字符串类型
    return typeof current === 'string' ? current : undefined;
  }

  public getDependencyModules(): Module[] {
    let dependencies = this.getDependencies('dependencies');
    if (this._projectConfig) {
      dependencies = this.applyOverrides(dependencies);
    }
    if (!dependencies) {
        return [];
    }

    const dstDeps: Module[] = [];
    Object.entries(dependencies).forEach(([name, value]) => {
        if (typeof value === 'string') {
            const resolvedValue = this.replacePlaceholders(value);
            const projectRoot = path.dirname(this._filePath);
            dstDeps.push(this.genDstNode(name, resolvedValue, projectRoot));
        }
    });
    return dstDeps;
  }

  private genDstNode(key: string, value: string, modulePath: string): Module {
    if (!this._filePath) {
        throw new Error('Cannot generate destination node without the original file path.');
    }
    const ohPkgPath = this._filePath;

    if (value.startsWith('tag:')) {
        return {
            name: path.join(path.dirname(ohPkgPath), OH_MODULES_DIR, key),
            kind: ModuleCategory.TAGGED_PACKAGE,
            tag: value.replace(/^tag:/, ''),
        };
    } else if (/^(file:|\.\/|\.\.\/)/.test(value)) {
        const moduleSoPath = path.resolve(modulePath, value.replace(/^file:/, ''));
        return this.handleLocal(key, moduleSoPath);
    } else if (/^[~^0-9]/.test(value)) {
        return {
            name: path.join(path.dirname(ohPkgPath), OH_MODULES_DIR, key),
            kind: ModuleCategory.THIRD_PARTY_PACKAGE,
            tag: value,
        };
    } else {
        return {
            name: `Unknown: key is ${key}, value is ${value}, module path is ${path.dirname(ohPkgPath)}.`,
            kind: ModuleCategory.UNKNOWN,
        };
    }
  }

  private applyOverrides(dependencies: DependencyObject): DependencyObject {
    const overriddenDependencies = { ...dependencies };
    const overrides = this._projectConfig?.getOverrides();
    if (overrides) {
      for (const depName in overriddenDependencies) {
        if (overrides[depName]) {
          overriddenDependencies[depName] = overrides[depName];
        }
      }
    }
    return overriddenDependencies;
  }

  public getOverrides(): DependencyObject | undefined {
    if (this.getType() !== 'project') {
      return undefined;
    }
    return this._config.overrides;
  }

  private handleLocal(moduleName: string, modulePath: string): Module {
    if (!this._filePath) {
        throw new Error('Cannot handle local dependency without the original file path.');
    }
    const ohPkgPath = this._filePath;
    const moduleInstalledPath = path.join(path.dirname(ohPkgPath), OH_MODULES_DIR, moduleName);
    const originPkgPath = path.resolve(path.dirname(ohPkgPath), modulePath);
    try {
        const isDir = fs.statSync(originPkgPath).isDirectory();
        let moduleKind = getModuleKind(moduleInstalledPath);
        if (moduleKind === ModuleCategory.UNKNOWN && (modulePath.endsWith('.hsp') || modulePath.endsWith('.tgz'))) {
            moduleKind = ModuleCategory.HSP;
        } else if (moduleKind === ModuleCategory.UNKNOWN && (modulePath.endsWith('.har'))) {
            moduleKind = ModuleCategory.HAR;
        }
        return {
            name: isDir ? originPkgPath : moduleInstalledPath,
            kind: moduleKind,
            originPath: isDir ? undefined : originPkgPath,
        };
    } catch (e) {
        return {
            name: `Unknown local dependency: ${moduleName} at ${modulePath}`,
            kind: ModuleCategory.UNKNOWN,
        };
    }
  }
}

// src/types.ts

export type DependencyObject = Record<string, string>;

export interface Author {
  name?: string;
  email?: string;
}

export type Scripts = Record<string, string>;

export type Hooks = {
  preInstall?: string;
  postInstall?: string;
  preUninstall?: string;
  postUninstall?: string;
  preVersion?: string;
  postVersion?: string;
  prePublish?: string;
  postPublish?: string;
};

export interface NativeComponent {
  name: string;
  compatibleSdkVersion?: string;
  compatibleSdkType?: string;
}

export interface ICommonConfig {
  description?: string;
  dependencies?: DependencyObject;
  devDependencies?: DependencyObject;
  dynamicDependencies?: DependencyObject;
  scripts?: Scripts;
  hooks?: Hooks;
}

export interface IProjectConfig extends ICommonConfig {
  modelVersion: string;
  overrides?: DependencyObject;
  overrideDependencyMap?: Record<string, string>;
  parameterFile?: string;
}

export interface IModuleConfig extends ICommonConfig {
  name: string;
  version: string;
  keywords?: string[];
  author?: string | Author;
  homepage?: string;
  repository?: string;
  license: string;
  main: string;
  types?: string;
  compatibleSdkVersion?: string;
  compatibleSdkType?: string;
  obfuscated?: boolean;
  nativeComponents?: NativeComponent[];
  artifactType?: 'original' | 'obfuscation';
  category?: string;
  packageType?: 'InterfaceHar';
}

export type OHPackageConfigType = 'project' | 'module';

export type ConfigData = Partial<IProjectConfig & IModuleConfig>;

export function findProjectConfig(startPath: string): OHPackageConfig | undefined {
  let currentPath = startPath;
  while (currentPath !== path.parse(currentPath).root) {
    const projectFilePath = path.join(currentPath, 'oh-package.json5');
    if (fs.existsSync(projectFilePath)) {
      const config = OHPackageConfig.fromFile(projectFilePath);
      if (config.getType() === 'project') {
        return config;
      }
    }
    currentPath = path.dirname(currentPath);
  }
  return undefined;
}
